package me.denley.preferencebinder;


import me.denley.preferencebinder.internal.PreferenceBinderProcessor;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilitySlice;
import ohos.agp.components.Component;
import ohos.agp.window.dialog.IDialog;
import ohos.app.Context;
import ohos.data.DatabaseHelper;
import ohos.data.preferences.Preferences;

import java.util.LinkedHashMap;
import java.util.Map;

public final class PreferenceBinder {
    private PreferenceBinder(){
        throw new AssertionError("Instances are not allowed");
    }

    /** DO NOT USE: Exposed for generated code. */
    public interface Binder<T> {
        void bind(Context context, T target, Preferences prefs);
        void unbind(T target);
    }

    static final Map<Class<?>, Binder<Object>> BINDERS = new LinkedHashMap<Class<?>, Binder<Object>>();
    static final Binder<Object> NOP_BINDER = new Binder<Object>() {
        @Override public void bind(Context context, Object target, Preferences prefs) { }
        @Override public void unbind(Object target) {}
    };

    private static class BinderException extends RuntimeException{
        BinderException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    /**
     * Bind annotated fields and methods in the specified {@link Ability}.
     *
     * @param target Target activity for field binding.
     */
    public static void bind(Ability target) {
        bind(target, target);
    }


    /**
     * Bind annotated fields and methods in the specified {@link Component}.
     *
     * @param target Target view for field binding.
     */
    public static void bind(Component target) {
        bind(target.getContext(), target);
    }

    /**
     * Bind annotated fields and methods in the specified {@link IDialog}.
     * @param target Target dialog for field binding.
     * @param ctx The Context to use to load {@link Preferences} values.
     */
    public static void bind(IDialog target, Context ctx) {
        bind(ctx, target);
    }

    /**
     * Bind annotated fields and methods in the specified {@link AbilitySlice}.
     *
     * @param target Target fragment for field binding.
     */
    public static void bind(AbilitySlice target) {
        final Context context = target.getAbility();

        if(context==null) {
            throw new IllegalStateException("Fragment must be attached to an Activity before binding");
        }

        bind(context, target);
    }

    /**
     * Bind annotated fields and methods in the specified {@link Object}.
     *
     * @param context The Context to use to load {@link Preferences} values.
     * @param target Target for field binding.
     */
    public static void bind(Context context, Object target){
        DatabaseHelper databaseHelper = new DatabaseHelper(context);
        Preferences preferences = databaseHelper.getPreferences("default");
        bind(context, target, preferences);
    }

    /**
     * Bind annotated fields and methods in the specified {@link Ability}.
     *
     * @param target Target for field binding.
     * @param prefsFileName The name of the {@link Preferences} file to use.
     */
    public static void bind(Ability target, String prefsFileName) {
        bind(target, target, prefsFileName);
    }



    /**
     * Bind annotated fields and methods in the specified {@link Ability}.
     *
     * @param target Target for field binding.
     * @param prefsFileName The name of the {@link Preferences} file to use.
     */
    public static void bind(Component target, String prefsFileName) {
        bind(target.getContext(), target, prefsFileName);
    }

    /**
     * Bind annotated fields and methods in the specified {@link IDialog}.
     * @param target Target for field binding.
     * @param prefsFileName The name of the {@link Preferences} file to use.
     * @param ctx The Context to use to load {@link Preferences} values.
     */
    public static void bind(IDialog target, String prefsFileName, Context ctx) {
        bind(ctx, target, prefsFileName);
    }

    /**
     * Bind annotated fields and methods in the specified {@link AbilitySlice}.
     *
     * @param target Target for field binding.
     * @param prefsFileName The name of the {@link Preferences} file to use.
     */
    public static void bind(AbilitySlice target, String prefsFileName) {
        final Context context = target.getAbility();

        if(context==null) {
            throw new IllegalStateException("Fragment must be attached to an Activity before binding");
        }

        bind(context, target, prefsFileName);
    }

    /**
     * Bind annotated fields and methods in the specified {@link Object}.
     *
     * @param context The Context to use to load {@link Preferences} values.
     * @param target Target for field binding.
     * @param prefsFileName The name of the {@link Preferences} file to use.
     */
    public static void bind(Context context, Object target, String prefsFileName) {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);
        Preferences preferences = databaseHelper.getPreferences(prefsFileName);
        bind(context, target, preferences);

    }

    /**
     * Stops receiving preference value changes on the given target.
     *
     * @param target Target for field binding.
     */
    public static void unbind(Object target){
        Class<?> targetClass = target.getClass();

        Binder<Object> binder = BINDERS.get(targetClass);
        if (binder != null) {
            binder.unbind(target);
        }
    }

    /**
     * Bind annotated fields and methods in the specified {@link Object}.
     *
     * This method should only be used for unit-testing purposes (by providing a
     * mocked {@link Preferences} object).
     *
     * @param context The Context to use to load {@link Preferences} values.
     * @param target Target for field binding.
     * @param prefs The Preferences object from which to load preference values.
     */
    public static void bind(Context context, Object target, Preferences prefs) {
        Class<?> targetClass = target.getClass();

        try{
            Binder<Object> binder = findBinderForClass(targetClass);
            if (binder != null) {
                binder.bind(context, target, prefs);
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new BinderException("Unable to bind preferences for " + target, e);
        }
    }

    private static Binder<Object> findBinderForClass(Class<?> cls) throws IllegalAccessException, InstantiationException {
        Binder<Object> binder = BINDERS.get(cls);
        if (binder != null) {
            return binder;
        }
        String clsName = cls.getName();
        if (clsName.startsWith(PreferenceBinderProcessor.OPENHARMONY_PREFIX) || clsName.startsWith(PreferenceBinderProcessor.JAVA_PREFIX)) {
            return NOP_BINDER;
        }
        try {
            Class<?> binderClass = Class.forName(clsName + PreferenceBinderProcessor.SUFFIX);
            binder = (Binder<Object>) binderClass.newInstance();
        } catch (ClassNotFoundException e) {
            binder = findBinderForClass(cls.getSuperclass());
        }
        BINDERS.put(cls, binder);
        return binder;
    }

}
